import { CSSProperties, dom, EventHandler, Nodes } from '@knno/dom';
import { checkbox } from '../checkbox/checkbox';
import { iconStyle, vars } from '../global.css';
import SVGSort from '../../svg/sort-arrow.svg';
import SVGArrowUp from '@material-design-icons/svg/filled/keyboard_arrow_up.svg';
import SVGArrowDown from '@material-design-icons/svg/filled/keyboard_arrow_down.svg';
import { DragMask } from '../mask/mask';
import { size2px } from '../../tools/utils';
import { DATA } from '../symbol';

export type RowType = Record<string | number, unknown> | unknown;
export type SortType = 'simple' | 'combine' | 'none';
export type ColumnSizeType = 'scale' | 'fixed' | 'auto';
export type MeasureType =
	| 'resize'
	| 'headline'
	| 'sort'
	| 'col-resize'
	| 'col-fixed'
	| 'columns'
	| 'selection'
	| 'rows'
	| 'current-page'
	| 'refresh';
interface GridCellClickEventDetail {
	colIndex: number;
	rowIndex: number;
	row: RowType;
}

interface GridRowPressEventDetail {
	rowIndex: number;
	row: RowType;
	key: string;
	code: string;
}

interface GridReorderEventDetail {
	ordered: RowType[];
}

type GridCellClickEvent = CustomEvent<GridCellClickEventDetail>;
type GridRowPressEvent = CustomEvent<GridRowPressEventDetail>;
type GridSelectChangeEvent = CustomEvent<Set<unknown>>;
type GridReorderEvent = CustomEvent<GridReorderEventDetail>;

export interface GridEventMap {
	rowclick: GridCellClickEvent;
	rowpress: GridRowPressEvent;
	rowdblclick: GridCellClickEvent;
	selectchange: GridSelectChangeEvent;
	reorder: GridReorderEvent;
	scrollbottom: Event;
}

export type GridEventKey = keyof GridEventMap;
export type GridEventHandler<T extends GridEventKey, O extends Nodes> = (this: O, event: GridEventMap[T]) => void;

export interface Column {
	key?: string | number;
	title?: string;
	align?: 'left' | 'center' | 'right';
	width?: number | string;
	fixed?: boolean;
	/**
	 * Whether rows can be sorted by this column
	 */
	sortable?: boolean;
	orderBy?: 'asc' | 'desc' | 'none';
	/**
	 * Whether the column can be resize by user drag
	 */
	resizable?: boolean;
	/**
	 * Style will be apply to every column cells includ header cell
	 */
	style?: CSSProperties;
	/**
	 * Custom cell content
	 * @param value current cell value, if cell is in the head line, value is the column.title
	 * @param row current render line index, -1 is head line
	 * @param col current render column index
	 */
	formatter?: (row: RowType, cell: unknown, rowIndex: number, colIndex: number) => Element | Text | (Element | Text)[];
	/**
	 * provide custom compare function for sort
	 */
	compare?: (a: unknown, b: unknown) => number;
}

export function compare(a: unknown, b: unknown, col: Column): number {
	if (typeof col.compare === 'function') {
		return col.compare(a, b);
	} else {
		if ((a === null || a === undefined) && b !== null && b !== undefined) {
			return -1;
		} else if ((b === null || b === undefined) && a !== null && a !== undefined) {
			return 1;
		}
		if (typeof a === 'number' && typeof b === 'number') {
			return a - b;
		} else {
			return (a + '').localeCompare(b + '');
		}
	}
}

function getValue(row: RowType, col: Column): unknown {
	if (col.key === undefined) {
		return row;
	} else if (row === undefined || row === null) {
		return undefined;
	} else {
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		return (row as any)[col.key];
	}
}

export function genWidth(width: number | string | undefined): string {
	if (typeof width === 'number') {
		return `${Math.round(width)}px`;
	} else if (typeof width === 'string') {
		return width;
	} else {
		return '';
	}
}

type CellLike = { rowIdx: number; colIdx: number };

function findCell(element: Element | null): CellLike | null {
	// eslint-disable-next-line @typescript-eslint/no-explicit-any
	const isCell = (elem: any): elem is CellLike => {
		return elem.rowIdx >= 0 && elem.colIdx >= 0;
	};
	while (element && !isCell(element)) {
		element = element.parentElement;
	}
	return element;
}

export interface Section {
	key: string | number;
	align?: 'left' | 'center' | 'right';
	/**
	 * Style will be apply to every column cells includ header cell
	 */
	style?: CSSProperties;
	/**
	 * Custom cell content
	 * @param value current cell value, if cell is in the head line, value is the column.title
	 * @param row current render line index, -1 is head line
	 */
	formatter?: (value: unknown, row: number) => Element | Element[] | Text;
}

const MIN_WIDTH = 10;

export interface GridBase {
	on<K extends keyof GridEventMap>(
		event: K,
		listener: GridEventHandler<K, this>,
		options?: boolean | AddEventListenerOptions
	): this;
	on<K extends keyof HTMLElementEventMap>(
		event: K,
		listener: EventHandler<K, this>,
		options?: boolean | AddEventListenerOptions
	): this;

	off<K extends keyof GridEventMap>(
		event: K,
		listener: GridEventHandler<K, this>,
		options?: boolean | AddEventListenerOptions
	): this;
	off<K extends keyof HTMLElementEventMap>(
		event: K,
		listener: EventHandler<K, this>,
		options?: boolean | AddEventListenerOptions
	): this;
}

export interface GridBaseData {
	data: RowType[];
	fixed: number;
	sorted: RowType[] | null;
	columns: Column[];
	section: Section | null;
	selected: Set<unknown>;
	selectable: boolean;
	actived: number | null;
	checkcol: Column;
	disableField: number | string | null;
}

export abstract class GridBase extends Nodes {
	protected [DATA]: GridBaseData = {
		data: [],
		fixed: 0,
		sorted: null,
		columns: [],
		section: null,
		selected: new Set<unknown>(),
		selectable: false,
		actived: null,
		disableField: null,
		checkcol: {
			style: {
				borderRight: `1px solid ${vars.color.border}`,
			},
			align: 'center',
			width: '4rem',
			fixed: true,
			formatter: (_row, _cell, rowIdx) => {
				const rows = this.getSortedData();
				let checked: boolean | 'half';
				if (rowIdx < 0) {
					if (this[DATA].selected.size === 0) {
						checked = false;
					} else if (this[DATA].selected.size === rows.length) {
						checked = true;
					} else {
						checked = 'half';
					}
				} else {
					checked = this[DATA].selected.has(rows[rowIdx]);
				}
				return checkbox()
					.checked(checked)
					.ref((ck) => {
						ck.on('click', () => {
							if (rowIdx < 0) {
								if (!ck.checked()) {
									this[DATA].selected.clear();
								} else {
									this[DATA].selected = new Set(rows);
								}
							} else {
								if (ck.checked()) {
									this[DATA].selected.add(rows[rowIdx]);
								} else {
									this[DATA].selected.delete(rows[rowIdx]);
								}
							}
							this.renderSelect();
							const event = new CustomEvent('selectchange', {
								bubbles: false,
								detail: this[DATA].selected,
							});
							this.emit(event);
						});
						ck.on('mousedown', (evt) => {
							evt.preventDefault();
							(this[0] as HTMLElement).focus();
							this.active(rowIdx);
							ck.emit(new MouseEvent('click'));
						});
					});
			},
		},
	};

	// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
	protected measure(reason: MeasureType): void {}
	protected abstract genStyle(): boolean;
	protected abstract renderBody(): void;
	protected abstract renderSelect(): void;
	protected abstract renderHead(): void;
	protected abstract setActiveRow(rowIdx: number | null, focus: boolean): void;

	constructor(elements: Iterable<Element | Text>) {
		super(elements);
		this.attr('tabindex', '0');
		this.on('mousedown', (evt) => {
			if (this.activable()) {
				const elem = findCell(evt.target as Element | null);
				if (elem) {
					let rid: number | null;
					if (
						elem.rowIdx === this[DATA].actived &&
						this[0] === document.activeElement &&
						evt.ctrlKey &&
						evt.button === 0
					) {
						rid = null;
					} else {
						rid = elem.rowIdx;
						if (this.isRowDisableByIndex(rid)) {
							return;
						}
					}
					this.setActiveRow(rid, false);
				}
			}
		});

		this.on('keydown', (evt) => {
			if (!this.activable()) return;
			let row = this[DATA].actived;
			const data = this.getSortedData();
			if (evt.key === 'ArrowDown') {
				if (row === null) {
					row = 0;
				} else {
					if (row < data.length - 1) {
						row++;
					}
				}
				while (row < data.length) {
					if (this.isRowDisableByIndex(row)) {
						row++;
					} else {
						this.setActiveRow(row, true);
						break;
					}
				}
				evt.preventDefault();
				evt.stopPropagation();
			} else if (evt.key === 'ArrowUp') {
				if (row === null) {
					row = data.length - 1;
				} else {
					if (row > 0) {
						row--;
					}
				}
				while (row >= 0) {
					if (this.isRowDisableByIndex(row)) {
						row--;
					} else {
						this.setActiveRow(row, true);
						break;
					}
				}
				evt.preventDefault();
				evt.stopPropagation();
			}
		});

		this.on('keyup', (evt) => {
			if (evt.key === 'Enter') {
				evt.preventDefault();
				evt.stopPropagation();
			}
		});

		this.on('keypress', (evt) => {
			if (!this.activable()) return;
			const idx = this.active();
			if (idx === null) {
				return;
			}
			const row = this.getSortedData()[idx];
			// const row = this.activeRow();
			if (!row) {
				return;
			}
			if (this.isRowDisable(row)) {
				return;
			}
			evt.preventDefault();
			const event = new CustomEvent('rowpress', {
				...evt,
				bubbles: false,
				detail: {
					key: evt.key,
					code: evt.code,
					row,
					rowIndex: idx,
				},
			});
			this.emit(event);
		});

		this.on('click', (evt) => {
			const elem = findCell(evt.target as Element | null);
			if (elem) {
				const row = this.getSortedData()[elem.rowIdx];
				if (this.isRowDisable(row)) {
					return;
				}
				const event = new CustomEvent('rowclick', {
					...evt,
					bubbles: false,
					detail: {
						row,
						rowIndex: elem.rowIdx,
						colIndex: elem.colIdx,
					},
				});
				this.emit(event);
			}
		});

		this.on('dblclick', (evt) => {
			const elem = findCell(evt.target as Element | null);
			if (elem) {
				const row = this.getSortedData()[elem.rowIdx];
				if (this.isRowDisable(row)) {
					return;
				}
				const event = new CustomEvent('rowdblclick', {
					...evt,
					bubbles: false,
					detail: {
						row,
						rowIndex: elem.rowIdx,
						colIndex: elem.colIdx,
					},
				});
				this.emit(event);
			}
		});
	}

	protected adjustColumnSize() {
		if (!(this[0] as HTMLElement).isConnected) {
			return this.getColumns();
		}
		const total = this.innerWidth();
		// get all setted with
		let logicTotal = 0;
		let unspecified = 0;
		const columns = this.getColumns();
		columns.forEach((c) => {
			let width = size2px(c.width, this[0] as HTMLElement, false);
			if (width === undefined) {
				c.width = undefined;
				unspecified++;
			} else if (c.fixed) {
				logicTotal += width;
			} else {
				if (width < MIN_WIDTH) {
					width = MIN_WIDTH;
				}
				c.width = width;
				logicTotal += width;
			}
		});
		let unspecifyWidth = MIN_WIDTH;
		if (unspecified > 0) {
			unspecifyWidth = Math.floor((total - logicTotal) / unspecified) - 1;
		}
		if (unspecifyWidth < MIN_WIDTH) {
			unspecifyWidth = MIN_WIDTH;
		}
		logicTotal += unspecifyWidth * unspecified;
		if (this.columnSize() === 'scale') {
			// logic with to real with
			columns.forEach((c) => {
				if (c.width === undefined) {
					c.width = (unspecifyWidth / logicTotal) * 100 + '%';
				} else if (!c.fixed) {
					c.width = ((c.width as number) / logicTotal) * 100 + '%';
				}
			});
		} else {
			columns.forEach((c) => {
				if (c.width === undefined) {
					c.width = unspecifyWidth;
				}
			});
		}
		return columns;
	}

	protected getSortedData(): RowType[] {
		if (this[DATA].sorted) {
			return this[DATA].sorted;
		}
		const columns = this.getColumns();
		if (columns.filter((c) => c.orderBy === 'asc' || c.orderBy === 'desc').length === 0) {
			return this[DATA].data;
		}
		this[DATA].sorted = Array.from(this[DATA].data).sort((a, b) => {
			for (const c of columns) {
				if (c.key === undefined) continue;
				if (c.orderBy === 'asc') {
					const aValue = getValue(a, c);
					const bValue = getValue(b, c);
					const result = compare(aValue, bValue, c);
					if (result !== 0) {
						return result;
					}
				} else if (c.orderBy === 'desc') {
					const aValue = getValue(a, c);
					const bValue = getValue(b, c);
					const result = compare(bValue, aValue, c);
					if (result !== 0) {
						return result;
					}
				} else continue;
			}
			return 0;
		});
		return this[DATA].sorted;
	}

	protected getColumns(): Column[] {
		if (this.selectable()) {
			return [this[DATA].checkcol, ...this[DATA].columns];
		} else {
			return this[DATA].columns;
		}
	}

	protected isRowDisableByIndex(idx: number): boolean {
		return this.isRowDisable(this.getSortedData()[idx]);
	}

	protected isRowDisable(row: RowType | undefined | null): boolean {
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		return this[DATA].disableField !== null && row !== undefined && row !== null
			? !!(row as any)[this[DATA].disableField]
			: false;
	}

	protected renderSection(cell: Nodes, value: unknown, row: RowType, rowIdx: number, lastCol: Column): Nodes {
		const sec = this[DATA].section as Section;
		cell.class([sec.align ?? 'center', lastCol.resizable ? 'rb' : '']);
		cell.prop({ rowIdx, colIdx: 0 });
		cell.css('flex', '1');
		if (sec.style) {
			cell.css(sec.style);
		}
		if (typeof sec.formatter === 'function') {
			return cell.append(sec.formatter(value, rowIdx));
		} else {
			return cell.append(dom.text((value ?? '') + ''));
		}
	}

	protected renderCell(cell: Nodes, row: RowType, rowIdx: number, col: Column, colIdx: number): Nodes {
		// const cell = dom.td('c' + colIdx, col.resizable ? 'rb' : '', col.align ?? 'left');
		cell.class(['c' + colIdx, col.resizable ? 'rb' : '', col.align ?? 'left']);
		cell.prop({ rowIdx, colIdx });
		if (col.style) {
			cell.css(col.style);
		}
		const value = getValue(row, col);
		if (typeof col.formatter === 'function') {
			return cell.append(col.formatter(row, value, rowIdx, colIdx));
		} else {
			return cell.append(dom.text((value ?? '') + ''));
		}
	}

	protected renderHeadCell(cell: Nodes, col: Column, colIdx: number): Nodes {
		const sortIcon = () => {
			// return dom.span(`${iconStyle} sort ${col.orderBy ?? ''}`).html(SortIcon);
			let icon: string;
			if (col.orderBy === 'asc') {
				icon = SVGArrowDown;
			} else if (col.orderBy === 'desc') {
				icon = SVGArrowUp;
			} else {
				icon = SVGSort;
			}
			return dom.span(`${iconStyle} sort ${col.orderBy ?? ''}`).html(icon);
		};
		const dragHandler = () => {
			return dom
				.div('handle')
				.on('pointerdown', (evt) => {
					evt.preventDefault();
					evt.stopPropagation();
					const x = evt.clientX;
					const oldWidth = cell.prop('offsetWidth') as number;
					DragMask.show(evt, 'col-resize', (ev) => {
						const moveX = ev.clientX - x;
						col.width = oldWidth + moveX;
						if (col.width < 10) {
							col.width = 10;
						}
						if (this.genStyle()) {
							this.measure('col-resize');
							this.renderBody();
						}
					});
				})
				.on('touchmove', (evt) => {
					evt.preventDefault();
					evt.stopPropagation();
				});
		};
		const sortable = col.sortable && this.sortType() !== 'none';
		// const cell = dom.th('c' + colIdx, col.resizable ? 'rb' : '', sortable ? 'sort' : '');
		cell.class(['c' + colIdx, col.resizable ? 'rb' : '', sortable ? 'sort' : '', col.align ?? 'left']);
		if (col.style) {
			cell.css(col.style);
		}
		if (typeof col.formatter === 'function') {
			return cell.append(col.formatter({}, col.title, -1, colIdx));
		} else {
			return cell.append(
				dom
					.div()
					.append(
						dom
							.span(col.align ?? 'left')
							.css('flex', '1')
							.text(col.title ?? ''),
						sortable ? sortIcon() : []
					)
					.ref((span) => {
						if (sortable) {
							span.on('click', () => {
								const activedRow = this.activeRow();
								this[DATA].sorted = null;
								const srcOrder = col.orderBy;
								if (col.orderBy === 'none' || !col.orderBy) {
									col.orderBy = 'asc';
								} else if (col.orderBy === 'asc') {
									col.orderBy = 'desc';
								} else {
									col.orderBy = 'none';
								}
								if (
									this.sortType() !== 'combine' &&
									(srcOrder === 'none' || srcOrder === undefined) &&
									(col.orderBy === 'asc' || col.orderBy === 'desc')
								) {
									this.getColumns().forEach((c, i) => {
										if (i !== colIdx && c.sortable) {
											c.orderBy = 'none';
										}
									});
								}
								this.renderHead();
								this.genStyle();
								this.measure('sort');
								const orderedData = this.getSortedData();
								if (activedRow !== null) {
									this[DATA].actived = orderedData.indexOf(activedRow);
								}
								this.renderBody();
								const event = new CustomEvent('reorder', {
									bubbles: false,
									detail: {
										ordered: this.getSortedData(),
									},
								});
								this.emit(event);
							});
						}
					}),
				col.resizable ? dragHandler() : []
			);
		}
	}

	columnBorder(): boolean;
	columnBorder(value: boolean): this;
	columnBorder(value?: boolean): this | boolean {
		if (value === undefined) {
			return this.attr('column-border') != null;
		}
		this.attr('column-border', value);
		return this;
	}

	columnSize(): ColumnSizeType;
	columnSize(value: ColumnSizeType): this;
	columnSize(value?: ColumnSizeType): this | ColumnSizeType {
		const current = (this.attr('column-size') ?? 'auto') as ColumnSizeType;
		if (value === undefined) {
			return current;
		}
		value = /^(auto|fixed|scale)$/.test(value) ? value : 'auto';
		if (current === 'scale') {
			this.scrollLeft(0);
		}
		if (current === 'scale' && value === 'fixed') {
			this.attr('column-size', 'auto');
			this.renderHead();
			this.attr('column-size', 'fixed');
			if (this.genStyle()) {
				this.measure('col-resize');
				this.renderBody();
			}
		} else {
			this.attr('column-size', /^(auto|fixed|scale)$/.test(value) ? value : 'auto');
			this.renderHead();
			if (this.genStyle()) {
				this.measure('col-resize');
				this.renderBody();
			}
		}
		return this;
	}

	columns(): Column[];
	columns(cols: Column[]): this;
	columns(cols?: Column[]): this | Column[] {
		if (cols === undefined) {
			return this[DATA].columns;
		}
		this[DATA].columns = cols;
		this[DATA].sorted = null;
		this.renderHead();
		this.genStyle();
		this.measure('columns');
		this.renderBody();
		return this;
	}

	headLine(): boolean;
	headLine(value: boolean): this;
	headLine(value?: boolean): this | boolean {
		if (value === undefined) {
			return this.attr('show-head') != null;
		}
		this.attr('show-head', value);
		this.renderHead();
		if (this.genStyle()) {
			this.measure('headline');
			this.renderBody();
		}
		return this;
	}

	sortType(): SortType;
	sortType(value: SortType): this;
	sortType(value?: SortType): this | SortType {
		if (value === undefined) {
			return (this.attr('sort') ?? 'none') as SortType;
		}
		this.attr('sort', /^(simple|combine|none)$/.test(value) ? value : 'none');
		this[DATA].sorted = null;
		this.setActiveRow(null, false);
		this.renderHead();
		if (this.genStyle()) {
			this.measure('sort');
			this.renderBody();
		}
		return this;
	}

	selectable(): boolean;
	selectable(value: boolean): this;
	selectable(value?: boolean): this | boolean {
		if (value === undefined) {
			return this[DATA].selectable;
		}
		this[DATA].selectable = value;
		if (!value) {
			this[DATA].selected = new Set();
		}
		this.renderHead();
		this.genStyle();
		this.measure('columns');
		this.renderBody();
		return this;
	}

	selected(): RowType[];
	selected(rows: RowType[]): this;
	selected(rows?: RowType[]): this | RowType[] {
		if (rows === undefined) {
			return this.getSortedData().filter((row) => this[DATA].selected.has(row));
		}
		this[DATA].selected = new Set(rows);
		this.renderHead();
		this.genStyle();
		this.measure('selection');
		this.renderBody();
		return this;
	}

	activable(): boolean;
	activable(value: boolean): this;
	activable(value?: boolean): this | boolean {
		if (value === undefined) {
			return this.attr('activable') != null;
		}
		this.attr('activable', value);
		if (!value) {
			this[DATA].actived = null;
		}
		if (this[DATA].actived !== null && this.isRowDisableByIndex(this[DATA].actived)) {
			this[DATA].actived = null;
		}
		this.setActiveRow(this[DATA].actived, false);
		return this;
	}

	active(): number | null;
	active(pos: number | null, focus?: boolean): this;
	active(pos?: number | null, focus = false): this | number | null {
		if (pos === undefined) {
			return this[DATA].actived;
		}
		if (this.activable() && (pos === null || (pos >= 0 && pos < this.getSortedData().length))) {
			if (pos === null || !this.isRowDisableByIndex(pos)) {
				this.setActiveRow(pos, focus);
			}
		}
		return this;
	}

	activeRow(): RowType | null {
		if (this[DATA].actived === null) return null;
		return this.getSortedData()[this[DATA].actived];
	}

	rows(): RowType[];
	rows(rows: RowType[]): this;
	rows(rows?: RowType[]): this | RowType[] {
		if (rows === undefined) {
			return this[DATA].data;
		}
		this[DATA].data = rows;
		this[DATA].sorted = null;
		this.setActiveRow(null, false);
		this.measure('rows');
		this.renderBody();
		return this;
	}

	showEmpty(): boolean;
	showEmpty(value: boolean): this;
	showEmpty(value?: boolean): this | boolean {
		if (value === undefined) {
			return this.attr('show-empty') != null;
		}
		this.attr('show-empty', value);
		if (this.rows().length === 0) {
			this.measure('rows');
			this.renderBody();
		}
		return this;
	}

	fixColumns(): number;
	fixColumns(value: number): this;
	fixColumns(value?: number): this | number {
		if (value === undefined) {
			return this[DATA].fixed;
		}
		if (value < 0 || isNaN(value)) value = 0;
		this[DATA].fixed = value;
		if (this.genStyle()) {
			this.measure('col-fixed');
			this.renderBody();
		}
		return this;
	}

	section(): Section | null;
	section(value: Section | null): this;
	section(value?: Section | null): Section | null | this {
		if (value === undefined) {
			return this[DATA].section;
		} else {
			this[DATA].section = value;
			this.measure('rows');
			this.renderBody();
			return this;
		}
	}

	disableOn(): string | number | null;
	disableOn(value: string | number | null): this;
	disableOn(value?: string | number | null): string | number | null | this {
		if (value === undefined) {
			return this[DATA].disableField;
		} else {
			this[DATA].disableField = value;
			this.renderBody();
			return this;
		}
	}

	refresh(type: 'complete' | 'data' = 'complete'): this {
		if (type === 'complete') {
			this.renderHead();
			this.genStyle();
			this.measure('refresh');
		} else {
			this.measure('current-page');
		}
		this[DATA].sorted = null;
		this.renderBody();
		return this;
	}
}
